using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Drawing;
using System.IO;
using Krilib;
using Krilib.Bridges;

namespace Krile.Cores
{
    /// <summary>
    /// Image cache server
    /// </summary>
    internal class ImageManager : IImageManager
    {
        private List<GetCredentialCallbackDelegate> credentialCallbackList;

        private Dictionary<Uri, CacheData> cache;

        private Dictionary<Uri, List<GetImageCallbackDelegate>> callbackWaiters;

        private Queue<Uri> reserves;

        private volatile bool _downloading = false;

        #region locking objects
        //for lock statements

        private static object cacheAccessLocker = new object();

        private static object callbackAccessLocker = new object();

        private static object reserveAccessLocker = new object();

        #endregion

        /// <summary>
        /// Downloading now?
        /// </summary>
        internal bool Downloading
        {
            get { return _downloading; }
            private set
            {
                _downloading = value;
                if (DownloadStateChanged != null)
                    DownloadStateChanged.Invoke(this, new EventArgs());
            }
        }

        /// <summary>
        /// Download state changed.
        /// </summary>
        internal event EventHandler DownloadStateChanged;

        /// <summary>
        /// Constructor
        /// </summary>
        internal ImageManager()
        {
            //Initialize valuables
            credentialCallbackList = new List<GetCredentialCallbackDelegate>();
            cache = new Dictionary<Uri, CacheData>();
            callbackWaiters = new Dictionary<Uri, List<GetImageCallbackDelegate>>();
            reserves = new Queue<Uri>();

            //Create cache directory
            Directory.CreateDirectory(Path.Combine(Define.AppPath, Define.ImgCacheDir));

            //Load previous cache
            string prevcachefile = Path.Combine(Define.AppPath, Define.PrevCacheData);
            if (File.Exists(prevcachefile))
            {
                string text;
                using (StreamReader sr = new StreamReader(prevcachefile, Encoding.UTF8))
                {
                    text = sr.ReadToEnd();
                }
                LoadPrevCacheData(text);
            }

            System.Windows.Forms.Application.ApplicationExit += new EventHandler(Application_ApplicationExit);
        }

        #region PrevCacheSaveLoadAutomation

        void Application_ApplicationExit(object sender, EventArgs e)
        {
            //cleaning
            CollectGarbage();

            //save cachedata
            string prevcachefile = Path.Combine(Define.AppPath, Define.PrevCacheData);
            using (StreamWriter sw = new StreamWriter(prevcachefile, false, Encoding.UTF8))
                sw.Write(CacheDataStr());
        }

        /// <summary>
        /// Cache management data
        /// </summary>
        /// <returns>data string</returns>
        internal string CacheDataStr()
        {
            StringBuilder sb = new StringBuilder();
            lock (cacheAccessLocker)
            {
                foreach (Uri u in cache.Keys)
                {
                    if (cache[u].cacheresult.Succeeded)
                    {
                        sb.Append(u.OriginalString + '\t' +
                            cache[u].timestamp + '\t' +
                            GetImgPath(u) + Environment.NewLine);
                    }
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// Load previous cache data
        /// </summary>
        /// <remarks>
        /// Warning:This method will lock cache access while running in this method.
        /// </remarks>
        /// <param name="data">prevcachedatastr</param>
        internal void LoadPrevCacheData(string data)
        {
            lock (cacheAccessLocker)
            {
                string[] cachedata = data.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string str in cachedata)
                {
                    //0:Uri 1:Timestamp 2:Path
                    string[] utp = str.Split('\t');

                    //check length
                    if (utp.Length != 3) continue;

                    //check data
                    if (!Uri.IsWellFormedUriString(utp[0], UriKind.Absolute) ||
                        GetImgPath(new Uri(utp[0])) != utp[2] ||
                        !File.Exists(utp[2]) ||
                        cache.ContainsKey(new Uri(utp[0])))
                        continue;

                    //make operationresult
                    CacheData cd = new CacheData(new Uri(utp[0]), DateTime.Parse(utp[1]), utp[2]);
                    cache.Add(new Uri(utp[0]), cd);
                }
            }
        }

        #endregion

        #region TimeStampControl

        /// <summary>
        /// Destroy cache which exceeded max keeping time
        /// </summary>
        internal void CollectGarbage()
        {
            lock (cacheAccessLocker)
            {
                //delete targets array
                List<Uri> deltarget = new List<Uri>();

                foreach (Uri u in cache.Keys)
                {
                    //cache which have no timestamp or cache which exceeded max keeping time will be remove.
                    if (!CheckTimestamp(cache[u].timestamp))
                        deltarget.Add(u);
                }

                //execute remove cache
                foreach (Uri u in deltarget)
                {
                    DestroyCache(u);
                }
            }
        }

        /// <summary>
        /// Check cache uri's timestamp if it exceeds keeping limit.
        /// </summary>
        /// <param name="t">timestamp</param>
        /// <returns>alive:true exceed:false</returns>
        private bool CheckTimestamp(DateTime t)
        {
            return ((DateTime)DateTime.Now.Subtract(
                    new TimeSpan(
                    0, Core.Config.Behavior.PictureSaveLimitMinutes, 0))).CompareTo(t) < 0;
        }

        #endregion

        #region PathControl

        /// <summary>
        /// Create full path by uri
        /// </summary>
        /// <param name="uri">uri</param>
        /// <returns>full path</returns>
        internal static string GetImgPath(Uri uri)
        {
            return Path.Combine(Define.AppPath,
                Path.Combine(Define.ImgCacheDir, CreateFilenameByUri(uri)));
        }

        /// <summary>
        /// Create filename by uri
        /// </summary>
        /// <param name="uri">uri</param>
        /// <returns>filename</returns>
        private static string CreateFilenameByUri(Uri uri)
        {
            //Create unique name by md5

            byte[] strbyte = Encoding.ASCII.GetBytes(uri.OriginalString);
            //Hash key is "her" name.
            using (System.Security.Cryptography.HMACMD5 mdfive =
                new System.Security.Cryptography.HMACMD5(Encoding.ASCII.GetBytes("Krile Shikishima")))
            {
                byte[] output = mdfive.ComputeHash(strbyte);
                return BitConverter.ToString(output).ToLower().Replace("-", String.Empty);
            }
        }

        #endregion

        /// <summary>
        /// Add delegate to credential callback list
        /// </summary>
        /// <param name="credcback"></param>
        public void AddCredentialCallbackList(GetCredentialCallbackDelegate credcback)
        {
            credentialCallbackList.Add(credcback);
        }

        /// <summary>
        /// Destroy image cache
        /// </summary>
        /// <param name="uri">target uri</param>
        public void DestroyCache(Uri uri)
        {
            if (cache.ContainsKey(uri))
            {
                lock (cacheAccessLocker)
                    cache.Remove(uri);
            }
        }

        /// <summary>
        /// Get image with current thread
        /// </summary>
        /// <param name="uri">target uri</param>
        /// <param name="refresh">re-download image</param>
        /// <returns>result</returns>
        public Image GetImage(Uri uri, bool refresh)
        {
            if (!refresh && IsCacheExists(uri))
            {
                lock (cacheAccessLocker)
                    if (cache[uri].Image != null)
                        return (Image)cache[uri].Image.Clone();
                    else
                        return null;
            }
            else
            {
                OperationResult<Image> ret = DownloadImage(uri);
                lock (cacheAccessLocker)
                    cache.Add(uri, new CacheData(ret));
                return ret.Data;
            }
        }

        /// <summary>
        /// Get image with callback
        /// </summary>
        /// <param name="uri">target uri</param>
        /// <param name="refresh">re-download</param>
        /// <param name="callback">callback method's delegate</param>
        public void GetImageCallback(Uri uri, bool refresh, GetImageCallbackDelegate callback)
        {
            if (!refresh && IsCacheExists(uri))
            {
                callback.Invoke(uri, cache[uri].Image);
                return;
            }

            lock (callbackAccessLocker)
            {
                if (!callbackWaiters.ContainsKey(uri))
                {
                    callbackWaiters.Add(uri, new List<GetImageCallbackDelegate>());
                }
                callbackWaiters[uri].Add(callback);
            }

            if (IsReserved(uri))
                return;

            lock (reserveAccessLocker)
            {
                reserves.Enqueue(uri);
            }

            //Prevention for UI thread freezing
            ProcessReserve();
        }

        private volatile int downloadCounter = 0;

        /// <summary>
        /// Download reserved images
        /// </summary>
        private void ProcessReserve()
        {
            Uri target = null;
            lock (reserveAccessLocker)
            {
                //Check begin parameter
                if (reserves.Count == 0 || downloadCounter > Core.Config.Network.ImgParallelDlMax)
                    return;

                //Get next uri
                target = reserves.Dequeue();
            }

            //Download state update
            downloadCounter++;
            Downloading = true;

            DownloadImageDelegate dlimgdelg = new DownloadImageDelegate(DownloadImage);
            dlimgdelg.BeginInvoke(target, new AsyncCallback(AsyncDLCallback), dlimgdelg);

            //Parallel thread dl
            ProcessReserve();
        }

        /// <summary>
        /// Image async download callback
        /// </summary>
        /// <param name="result">asyncresult object</param>
        private void AsyncDLCallback(IAsyncResult result)
        {
            //Get return value
            OperationResult<Image> ret = ((DownloadImageDelegate)result.AsyncState).EndInvoke(result);
            downloadCounter--;

            if (ret.Message != "ALREADY_EXISTS")
            {
                //Add cache data
                lock (cacheAccessLocker)
                {
                    if (cache.ContainsKey(ret.OperationUri))
                    {
                        var prev = cache[ret.OperationUri];
                        cache[ret.OperationUri] = new CacheData(ret);
                    }
                    else
                        cache.Add(ret.OperationUri, new CacheData(ret));
                }

                //Update download state
                if (downloadCounter == 0) Downloading = false;

            }

            //Begin callback
            DoCallback(ret.OperationUri);
            ProcessReserve();
        }

        /// <summary>
        /// Invoke image get callback
        /// </summary>
        /// <param name="uri">target uri</param>
        private void DoCallback(Uri uri)
        {
            if (!IsCacheExists(uri))
            {
                if (!IsReserved(uri))
                    ReserveImage(uri, false);
                return;
            }

            GetImageCallbackDelegate[] callbacks = null;
            lock (callbackAccessLocker)
            {
                if (!callbackWaiters.ContainsKey(uri)) return;

                callbacks = callbackWaiters[uri].ToArray();
                callbackWaiters.Remove(uri);
            }
            if (callbacks.Length == 0) return;
            foreach (GetImageCallbackDelegate callback in callbacks)
            {
                callback.Invoke(uri, cache[uri].Image);
            }
        }

        delegate OperationResult<Image> DownloadImageDelegate(Uri uri);
        /// <summary>
        /// Download image
        /// </summary>
        /// <param name="uri">target uri</param>
        /// <returns>operation result</returns>
        private OperationResult<Image> DownloadImage(Uri uri)
        {
            OperationResult<Image> ret = new OperationResult<Image>();
            ret.OperationUri = uri;
            if (IsCacheExists(uri))
            {
                ret.Message = "ALREADY_EXISTS";
                return ret;
            }
            try
            {
                string file = CreateFilenameByUri(uri);

                //Create request
                HttpWebRequest req = (HttpWebRequest)System.Net.WebRequest.Create(uri);
                req.UserAgent = Define.UserAgent;
                req.Timeout = Core.Config.Network.Timeout;
                if (Core.Config.Network.WebProxy == null)
                    req.Proxy = null;
                else if (Core.Config.Network.WebProxy == System.Net.WebRequest.DefaultWebProxy)
                    req.Proxy = System.Net.WebRequest.DefaultWebProxy;
                else
                    req.Proxy = new System.Net.WebProxy(
                        ((WebProxy)Core.Config.Network.WebProxy).Address,
                        ((WebProxy)Core.Config.Network.WebProxy).BypassProxyOnLocal,
                        ((WebProxy)Core.Config.Network.WebProxy).BypassList);

                //Use gzip compression
                if (Core.Config.Network.UseGZipCompression)
                    req.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;

                //Credential search
                for (int id = 0; id < credentialCallbackList.Count; id++)
                {
                    NetworkCredential nc = (NetworkCredential)credentialCallbackList[id].Invoke(uri);
                    if (nc != null)
                    {
                        req.Credentials = nc;
                        break;
                    }
                }

                //Get Responce
                HttpWebResponse res = (HttpWebResponse)req.GetResponse();

                using (Stream sm = res.GetResponseStream())
                {
                    //Get image
                    ret.Data = Image.FromStream(sm);
                }

                ret.Succeeded = true;
                return ret;
            }
            catch (Exception e)
            {
                Subsystem.Debugger.AddReport(e);
                ret.Exception = e;
                ret.Succeeded = false;
                ret.Message = e.Message;
                ret.ErrorOperation = OperationResult<Image>.ErrorOperations.Inform;
                return ret;
            }
        }

        /// <summary>
        /// Check cache exists
        /// </summary>
        /// <param name="uri">uri</param>
        /// <returns>cache exists</returns>
        public bool IsCacheExists(Uri uri)
        {
            if (uri == null)
                throw new ArgumentNullException("uri");
            DateTime tstamp = DateTime.Now;
            lock (cacheAccessLocker)
            {
                if (!cache.ContainsKey(uri)) return false;
                tstamp = cache[uri].timestamp;
            }

            if (CheckTimestamp(tstamp))
            {
                return true;
            }
            else
            {
                //Destroy cache and return false because it is an old cache.
                DestroyCache(uri);
                return false;
            }
        }

        /// <summary>
        /// Check uri is already reserved
        /// </summary>
        /// <param name="uri">uri</param>
        /// <returns>reserved:true notfound:false</returns>
        /// <remarks>you MUST NOT use in "ReserveAccessLocker" critical section.</remarks>
        public bool IsReserved(Uri uri)
        {
            lock (reserveAccessLocker)
            {
                return reserves.Contains(uri);
            }
        }

        private bool InternalIsReserved(Uri uri)
        {
            return reserves.Contains(uri);
        }

        /// <summary>
        /// Add reserve image
        /// </summary>
        /// <remarks>
        /// This method never mind downloading image state.
        /// Warning!If you reserve same uri again and again,you may make server downed!
        /// </remarks>
        /// <param name="uri">add uri</param>
        /// <param name="refresh">require refresh</param>
        public void ReserveImage(Uri uri, bool refresh)
        {
            lock (reserveAccessLocker)
            {
                if (!refresh && IsCacheExists(uri) || InternalIsReserved(uri))
                {
                    return;
                }
                reserves.Enqueue(uri);
            }
            ProcessReserve();
        }
    }

    /// <summary>
    /// Image cache data
    /// </summary>
    internal class CacheData
    {
        public OperationResult<object> cacheresult = null;

        /// <summary>
        /// weak reference for "auto dispose image"
        /// </summary>
        private WeakReference weakReference;

        object readingImage = new object();
        public Image Image
        {
            get
            {
                if (!cacheresult.Succeeded) return null;
                if (weakReference != null)
                {
                    lock (readingImage)
                    {
                        AutoDisposeImage targetAdi = (AutoDisposeImage)weakReference.Target;
                        if (targetAdi != null)
                            return (Image)targetAdi.KeepImage.Clone();
                    }
                }

                //failure get image
                lock (readingImage)
                {
                    AutoDisposeImage adi = new AutoDisposeImage(Krile.Imported.Snippets.Images.ImageSafeReader(ImageManager.GetImgPath(cacheresult.OperationUri), false));
                    weakReference = new WeakReference(adi);
                    return (Image)adi.KeepImage.Clone();
                }
            }
        }

        public DateTime timestamp = DateTime.Now;
        public CacheData() { }

        public CacheData(OperationResult<Image> image)
        {
            timestamp = DateTime.Now;
            cacheresult = new OperationResult<object>();
            cacheresult.OperationUri = image.OperationUri;
            cacheresult.Succeeded = image.Succeeded;
            cacheresult.Exception = image.Exception;
            cacheresult.ErrorOperation = OperationResult<object>.ErrorOperations.Ignore;
            cacheresult.Message = image.Message;
            if (image.Succeeded)
            {
                try
                {
                    image.Data.Save(ImageManager.GetImgPath(image.OperationUri));
                }
                catch (Exception e)
                {
                    Subsystem.Debugger.AddReport(e);
                }

                weakReference = new WeakReference(new AutoDisposeImage(image.Data));
                image.Data.Dispose();
            }
        }

        public CacheData(Uri ouri, DateTime ts, string path)
        {
            timestamp = ts;
            cacheresult = new OperationResult<object>();
            cacheresult.OperationUri = ouri;
            cacheresult.Succeeded = true;
            weakReference = null;
        }
    }

    /// <summary>
    /// Image with auto dispose(for WeakReference)
    /// </summary>
    internal class AutoDisposeImage
    {
        private Image keepimg;
        internal Image KeepImage
        {
            get { return keepimg; }
        }

        /// <summary>
        /// Create AutoDisposableImage Instance
        /// </summary>
        /// <param name="img">keep image(uses cloning copy in internal)</param>
        internal AutoDisposeImage(Image img)
        {
            keepimg = (Image)img.Clone();
        }

        ~AutoDisposeImage()
        {
            keepimg.Dispose();
        }
    }
}

